﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;

namespace BeanboxSoftware.BeanProxy.Internals
{
  public class MethodPartBase
  {
    public Type Type { get; set; }
    public virtual bool IsSpecific { get { return !Type.IsGenericParameter && !Type.ContainsGenericParameters; } }
    public int TemplatePosition { get { return Type.IsGenericParameter ? Type.GenericParameterPosition : -1; } }
    public virtual List<Type> Templates { get { return Type.GetGenericArguments().ToList(); } }
  }

  public class MethodPart : MethodPartBase
  {
    public bool IsReturn { get; set; }
    public bool IsOut { get; set; }
    public bool IsRef { get; set; }

    public MethodPart(Type type, bool isOut, bool isRet)
    {
      Type = type;
      IsReturn = isRet;
      IsOut = IsOut;
      IsRef = Type.IsByRef;
    }
    public MethodPart(ParameterInfo pi)
      : this(pi.ParameterType, pi.IsOut, false)
    {
    }
    public MethodPart(MethodPart copy)
    {
      Type = copy.Type;
      IsReturn = copy.IsReturn;
      IsOut = copy.IsOut;
      IsRef = copy.IsRef;
    }

    public override string ToString()
    {
      return Type == null ? "undefined method part" : Type.Name;
    }

    public int DepthToCast(MethodPart target)
    {
      return DepthToCast(target, null);
    }

    //public MethodPart DefinedAs(MethodPart target, Type[] inferred)
    public int DepthToCast(MethodPart target, Type[] inferred)
    {
      if (!target.IsSpecific)
        //return null;
        return -1;

      //Type newType = Type.DefinedAs(target.Type, inferred);
      int depth = Type.DepthToCast(target.Type, inferred);

      //if (newType == null)
      if (depth == -1)
        //return null;
        return -1;

      //return new MethodPart(target) { InferredDepth = depth };
      return depth;
    }

    public bool Matches(MethodPart it)
    {
      if (IsRef != it.IsRef)
        return false;

      if (IsOut != it.IsOut)
        return false;

      if (IsReturn != it.IsReturn) //not likely to happen internally - else there is a bug
        throw new ArgumentException("It is not expected for MethodDefinition to compare a return parameter to a non return parameter");

      return TypesMatch(Type, it.Type); //works for specific and non-specific
    }

    private static bool TypeIsSpecific(Type a)
    {
      return !a.ContainsGenericParameters && !a.IsGenericParameter;
    }

    public static bool TypesMatch(Type a, Type b)
    {
      //we expect out, ref, and return type has been verified to allow this to be recursive

      if (a.IsByRef != b.IsByRef)
        return false;

      if (a.IsByRef) //that means they both are
      {
        a = a.GetElementType(); //remove the ref attribute
        b = b.GetElementType();
      }

      if (TypeIsSpecific(a))
      {
        if (!TypeIsSpecific(b))
        {
          return false;
        }

        return a == b; //specific == specific
      }

      //we now know both are !specific
      else if (a.IsGenericParameter) //e.g. a is T
      {
        if (!b.IsGenericParameter) //e.g. is no just T, but perhaps List<T>
        {
          return false;
        }

        return a.GenericParameterPosition == b.GenericParameterPosition;
      }
      //b may be a generic parameter
      else if (b.IsGenericParameter)
      {
        return false;
      }
      //we now know both contain a generic, e.g. List<T>
      //The generic type must == specific's generic type defintion
      else
      {
        //e.g. List<> == List<>
        if (a.GetGenericTypeDefinition() != b.GetGenericTypeDefinition())
        {
          return false;
        }

        var aList = a.GetGenericArguments().ToList();
        var bList = b.GetGenericArguments().ToList();

        //if they have the same number of generic args (not sure if this is possible since the
        //generic types match
        if (aList.Count != bList.Count)
        {
          return false;
        }

        for (int i = 0; i < aList.Count; i++)
        {
          var ai = aList[i];
          var bi = bList[i];

          if (!TypesMatch(ai, bi))
            return false;
        }
      }

      return true;
    }
  }

  public class MethodDefinition : MethodPartBase
  {
    public Guid Id { get; set; }
    public MethodConfig MethodConfig { get; set; }
    public MethodInfo MethodInfo { get; set; }
    public string Name { get; set; }
    public object Target { get; set; }
    public int InferredDepth { get; set; }
    public override bool IsSpecific
    {
      get
      {
        return MethodInfo.GetGenericArguments().Count(t => t.FullName == null) == 0;
      }
    }
    public override List<Type> Templates
    {
      get
      {
        return MethodInfo.GetGenericArguments().ToList();
      }
    }

    /// <summary>
    /// Also means it is new
    /// </summary>
    public bool Unbound { get { return MethodConfig == null; } }

    //the goods
    public MethodPart Return { get; set; }
    public List<MethodPart> Parameters { get; set; }

    public MethodDefinition Parent { get; set; }
    public List<MethodDefinition> Specializations { get; set; }

    public IEnumerable<MethodPart> Parts
    {
      get
      {
        yield return Return;

        foreach (var parm in Parameters)
          yield return parm;
      }
    }

    public MethodDefinition()
    {
      Id = Guid.NewGuid();
      //MethodConfig
      //MethodInfo
      //Return
      Parameters = new List<MethodPart>();
      Specializations = new List<MethodDefinition>();
      Parent = this;
    }

    public MethodDefinition(MethodInfo methodInfo)
      : this()
    {
      Name = methodInfo.Name;
      Type = methodInfo.GetType();
      Apply(methodInfo);
    }

    public MethodDefinition(Type delegateType, string name)
      : this()
    {
      Type = delegateType;
      Name = name;
      Apply(delegateType);
    }

    public void Apply(MethodInfo methodInfo)
    {
      MethodInfo = methodInfo;

      Return = new MethodPart(methodInfo.ReturnType, false, true);

      foreach (var p in methodInfo.GetParameters())
      {
        var part = new MethodPart(p);
        Parameters.Add(part);
      }
    }

    public void Apply(Type delegateType)
    {
      var sig = MethodConfig.GetInvoker(delegateType);
      Apply(sig);
    }

    public void BindTo(object target)
    {
      Target = target;

      MethodConfig = new MethodConfig();
      MethodConfig.Initialize(target, MethodInfo);
    }

    public MethodDefinition DefinedAs(MethodDefinition signature, params Type[] types)
    {
      //if the signature already isn't a candidate...
      if (!signature.IsCandidateOf(this))
      {
        return null;
      }

      var sigParts = signature.Parts.ToList();
      var parts = Parts.ToList();

      //if we need types, try to infer them from the signature
      int totalDepth = 0;
      if (!IsSpecific && types.Length == 0)
      {
        Type[] inferred = new Type[Templates.Count];

        for (int i = 0; i < sigParts.Count; i++)
        {
          var s = sigParts[i];
          var t = parts[i];

          if (!t.IsSpecific)
          {
            //var specialized = t.DefinedAs(s, inferred);
            int depth = t.DepthToCast(s, inferred);

            //if (specialized == null)
            if (depth == -1)
            {
              return null;
            }

            totalDepth += depth;
          }
        }

        types = inferred;
      }

      //if any types are null, it cannot be defined
      if (types.Count(t => t == null) > 0)
        return null;

      var spDef = DefinedAs(types);
      spDef.InferredDepth = totalDepth;
      return spDef;
    }

    public MethodDefinition DefinedAs(params Type[] types)
    {
      //if template types were given, only definitions with of equal type
      if (types.Length != 0 && types.Length != Templates.Count)
      {
        return null;
      }

      if (types.Length != 0 && IsSpecific)
      {
        return null;
      }

      //no work to do
      if (IsSpecific)
      {
        return this;
      }

      //if it is specific and therefore no types given, return now to save work
      if (types.Length == 0)
      {
        return this;
      }

      //we may have already defined an instance for these types
      var specialized = Specific(types);

      //if nulll, we need to make one
      if (specialized == null)
      {
        var newInfo = MethodInfo.MakeGenericMethod(types);
        specialized = new MethodDefinition(newInfo);
        specialized.Parent = this;
        Specializations.Add(specialized);
      }

      if (!specialized.IsSpecific)
      {
        return null;
      }

      return specialized;
    }

    public override string ToString()
    {
      if (MethodInfo == null)
      {
        return "undefined method definition";
      }

      string name = MethodInfo.ToString();
      if (MethodInfo.DeclaringType.BaseType == typeof(MulticastDelegate))
      {
        string pattern = " {0}(";
        name = name.Replace(
          string.Format(pattern, MethodInfo.Name),
          string.Format(pattern, Name));
      }
      return name;
    }

    public MethodDefinition Specialize(params Type[] types)
    {
      if (types.Length == 0 && !IsSpecific)
      {
        return null;
      }
      else if (IsSpecific)
      {
        return this;
      }

      foreach (var sp in Specializations)
      {
        bool found = true;

        for (int i = 0; i < sp.Templates.Count; i++)
        {
          var templ = sp.Templates[i];
          var given = types[i];

          if (templ != given)
          {
            found = false;
            break;
          }
        }

        if (found)
        {
          return sp;
        }
      }

      return null;
    }

    public bool Matches(MethodDefinition it)
    {
      if (IsSpecific ^ it.IsSpecific) //Matches works for two specifics
        return false;

      if (!IsSpecific) //use dual generic matching
        return IsGenericMatch(it);

      if (Name != it.Name)
        return false;

      int count = Parameters.Count;

      if (count != it.Parameters.Count)
        return false;

      for (int i = 0; i < count; i++)
      {
        if (Parameters[i].Type != it.Parameters[i].Type)
          return false;

        if (Parameters[i].IsRef != it.Parameters[i].IsRef)
          return false;

        if (Parameters[i].IsOut != it.Parameters[i].IsOut)
          return false;
      }

      if (Return.Type != it.Return.Type)
        return false;

      return true;
    }

    public bool IsGenericMatch(MethodDefinition generic)
    {
      //you might notice i check name in each compare-like method
      //i'm sure i could remove one or two, but i want to be completely explicit
      if (Name != generic.Name)
        return false;

      int count = Templates.Count;

      if (count != generic.Templates.Count)
        return false;

      count = Parameters.Count;

      if (count != generic.Parameters.Count)
        return false;

      if (!Return.Matches(generic.Return))
        return false;

      for (int i = 0; i < count; i++)
      {
        if (!Parameters[i].Matches(generic.Parameters[i]))
          return false;
      }

      return true;
    }

    public bool IsCandidateOf(MethodDefinition generic)
    {
      if (!IsSpecific) // specific => specific ok, specific => generic ok, but generic => * no
        return false;

      if (generic.IsSpecific)
        return Matches(generic); // specific => specific is controlled by Matches

      if (Name != generic.Name)
        return false;

      int count = Parameters.Count;

      if (count != generic.Parameters.Count)
        return false;

      //type candidacy is more complicated
      //I use DepthToCast to support this. 
      //It returns -1 when a generic type cannot 'move' to a specific type
      if (generic.Return.DepthToCast(Return) == -1)
        return false;

      for (int i = 0; i < count; i++)
        if (generic.Parameters[i].DepthToCast(Parameters[i]) == -1)
          return false;

      return true;
    }

    public static MethodDefinition Lookup(Dictionary<Guid, MethodDefinition> defs, Guid id)
    {
      return defs[id];
    }

    public MethodDefinition Specific(Type[] templates)
    {
      int count = templates.Length;
      bool onlyGeneric = count > 0;

      if (IsSpecific)
      {
        if (onlyGeneric)
        {
          return null;
        }
        else
        {
          return this;
        }
      }
      else
      {
        foreach (var sp in Specializations)
        {
          if (onlyGeneric)
          {
            if (count != sp.Templates.Count)
            {
              continue;
            }

            bool valid = true;
            for (int i = 0; i < count; i++)
            {
              if (templates[i] != sp.Templates[i])
              {
                valid = false;
                break;
              }
            }
            if (!valid)
              continue;
          }

          return sp;
        }
      }

      return null;
    }
  }

  public static class MethodDefinitionExtension
  {
    /// <summary>
    /// A best fit is a candidate that needs to define the fewest undefined parts to fit
    /// </summary>
    /// <param name="methods">set of definitions to search from</param>
    /// <param name="def">the target signature</param>
    /// <param name="templates">an option list of part types</param>
    /// <returns>the best fit definition, or null if no candidates exist</returns>
    public static MethodDefinition BestFit(this IEnumerable<MethodDefinition> definitions, MethodDefinition signature, params Type[] templates)
    {
      var list = new List<MethodDefinition>();
      MethodDefinition best = null;

      int greatestDepth = int.MinValue;
      int leastDefines = int.MaxValue;

      foreach (var c in definitions.Candidates(signature, templates))
      {
        int currentDefines = c.Parent.Parameters.Where(p => !p.IsSpecific).Count();
        int currentDepth = c.InferredDepth;

        if (currentDefines > leastDefines)
        {
          continue; //not better
        }
        else if (currentDefines < leastDefines || currentDepth > greatestDepth)
        {
          list.Clear();
          list.Add(c);
          best = c;

          greatestDepth = currentDepth;
          leastDefines = currentDefines;
        }
        else if (currentDepth == greatestDepth) //ambiguous
        {
          list.Add(c);
        }
        //else same step count
      }

      if (best == null) //exit now
        return null;

      if (list.Count > 1)
      {
        throw new AmbiguousMatchException(
          string.Format("{0} and {1} are ambiguously matched in finding a best fit for {2} with the given types: {3}",
            list[0], list[1],
            signature, string.Join(", ", templates.Select(t => t.Name).ToArray())));
      }

      if (!best.IsSpecific)
      {
        throw new ArgumentException(string.Format("Failed to specialize {0} with types: {1}",
          best.ToString(), string.Join(", ", templates.Select(t => t.Name).ToArray())));
      }

      if (best.Unbound)
      {
        best.BindTo(best.Parent.Target);
      }

      return best;
    }

    public static MethodDefinition Find(this IEnumerable<MethodDefinition> methods, MethodInfo methodInfo)
    {
      var target = new MethodDefinition(methodInfo);

      foreach (var m in methods)
      {
        if (target.IsSpecific)
        {
          var sp = m.Specific(target.MethodInfo.GetGenericArguments());
          if (sp != null && sp.Matches(target))
          {
            return sp;
          }
        }
        else if (target.Matches(m))
        {
          return m;
        }
      }

      return null;
    }

    public static IEnumerable<MethodDefinition> Candidates(
      this IEnumerable<MethodDefinition> definitions,
      MethodDefinition signature,
      params Type[] templates)
    {
      foreach (var m in definitions)
      {
        if (m.Name != signature.Name)
          continue;

        var sp = m.Specific(templates);
        if (sp != null && sp.Matches(signature))
        {
          yield return sp;
          continue;
        }

        if (m.IsSpecific) //no need to create another specific
          continue;

        //apply templates
        var defined = m.DefinedAs(signature, templates);

        if (defined == null)
        {
          continue;
        }

        if (defined.Matches(signature))
        {
          yield return defined;
        }
      }
    }

    //public static Type DefinedAs(this Type generic, Type specific, Type[] inferred)
    public static int DepthToCast(this Type generic, Type specific, Type[] inferred)
    {
      if (!generic.ContainsGenericParameters && !generic.IsGenericParameter)
      {
        if (generic != specific) //can't cast types
        {
          return -1;
          //return null;
        }

        return 1;
        //return generic;
      }

      if (generic.IsGenericParameter)
      {
        if (inferred != null)
        {
          inferred[generic.GenericParameterPosition] = specific;
        }
        return 1;
        //return specific;
      }
      //if sp is not a generic type - we have a mismatch
      else if (!specific.IsGenericType)
      {
        return -1;
      }
      //must contain generics, and the generic type must == specific's generic type defintion
      //e.g. List<> == List<>
      else
      {
        if (generic.GetGenericTypeDefinition() != specific.GetGenericTypeDefinition())
        {
          return -1;
          //return null;
        }

        var genInnerList = generic.GetGenericArguments().ToList();
        var specInnerList = specific.GetGenericArguments().ToList();

        //if they have the same number of generic args (not sure if this is possible since the
        //generic types match
        if (genInnerList.Count != specInnerList.Count)
        {
          return -1;
          //return null;
        }

        int total = 1;
        for (int i = 0; i < genInnerList.Count; i++)
        {
          var genArg = genInnerList[i];
          var spcArg = specInnerList[i];

          var casted = genArg.DepthToCast(spcArg, inferred);
          //var casted = genArg.DefinedAs(spcArg, inferred);

          if (casted == -1)
          //if (casted == null) //failed
          {
            return -1;
            //return null;
          }

          total += casted;
        }

        return total;
        //return specific;
      }
    }
  }
}
